﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;
using System.ComponentModel;

using Zorbo;
using Zorbo.Interface;
using System.Linq.Expressions;

namespace Zorbo.Plugins
{
    public class PluginHost : IPluginHost
    {
        ObservableCollection<Plugin> plugins;

        public int Count {
            get { return plugins.Count((s) => s.Enabled); }
        }

        public IPlugin this[int index] {
            get {
                if (index >= 0 && index < plugins.Count)
                    lock (plugins) return plugins[index].Interface;
                    
                throw new ArgumentOutOfRangeException("index");
            }
        }

        public ReadOnlyList<Plugin> Plugins {
            get;
            private set;
        }

        internal IServer Server {
            get;
            private set;
        }


        class ErrorInfo : IErrorInfo 
        {
            public string Name { get; internal set; }

            public string Method { get; internal set; }

            public Exception Exception { get; internal set; }

            public ErrorInfo(string name, string method, Exception ex) {
                Name = name;
                Method = method;
                Exception = ex;
            }
        }


        public PluginHost(IServer server) {
            Server = server;

            plugins = new ObservableCollection<Plugin>();
            Plugins = new ReadOnlyList<Plugin>(plugins);

            plugins.CollectionChanged += Wrapper_CollectionChanged;
            ((INotifyPropertyChanged)plugins).PropertyChanged += Wrapper_PropertyChanged;
        }

        public bool Contains(IPlugin value) {
            lock (plugins)
                return plugins.Contains((s) => s.Interface == value);
        }

        public int IndexOf(IPlugin value) {
            lock (plugins)
                return plugins.FindIndex((s) => s.Interface == value);
        }


        public bool LoadPlugin(string name) {
            return LoadPlugin(name, false);
        }

        internal bool LoadPlugin(string name, bool dont_enable) {
            string lowname = name.ToLower();

            if (lowname.EndsWith(".dll")) {

                name = name.Substring(0, name.Length - 4);
                lowname = lowname.Substring(0, lowname.Length - 4);
            }

            lock (plugins) {
                
                int index = plugins.FindIndex(s => s.Name.ToLower() == lowname);

                if (index > -1) {
                    EnablePlugin(plugins[index]);
                    return true;
                }
            }

            String dir = Path.Combine(Directories.Plugins, name);
            String file = Path.Combine(dir, name + ".dll");

            if (!Directory.Exists(dir) || !File.Exists(file))
                return false;

            try {
                Assembly asm = Assembly.LoadFrom(file);

                foreach (var type in asm.GetExportedTypes()) {
                    if (typeof(IPlugin).IsAssignableFrom(type)) {

                        IPlugin plugin = (IPlugin)Activator.CreateInstance(type);

                        if (plugin.GUI != null) {
                            if (!(plugin.GUI is System.Windows.Forms.UserControl) &&
                                !(plugin.GUI is System.Windows.Controls.UserControl))
                                throw new PluginLoadException("Settings GUI MUST be a WPF or WinForms UserControl!");
                        }

                        Plugin zorbo_plugin = new Plugin(name, plugin);

                        lock (plugins) plugins.Add(zorbo_plugin);
                        if (!dont_enable) EnablePlugin(zorbo_plugin);

                        return true;
                    }
                }
            }
            catch (PluginLoadException plex) {
                OnError("LoadPlugin", "LoadPlugin", plex);
            }
            catch (Exception ex) {
                OnError("LoadPlugin", "LoadPlugin", new PluginLoadException("The plugin failed to load (See inner exception for details).", ex));
            }
            return false;
        }

        private void EnablePlugin(Plugin plugin) {
            try {
                plugin.Enabled = true;
                RaisePropertyChanged(() => Count);

                plugin.Interface.Directory = Path.Combine(Directories.Plugins, plugin.Name);
                plugin.Interface.OnPluginLoaded(Server);
            }
            catch (Exception ex) {
                OnError(plugin, "OnPluginLoaded", ex);
            }
        }


        public bool ImportPlugin(string path) {

            if (File.Exists(path)) {
                try {
                    FileInfo info = new FileInfo(path);

                    if (info.Extension != ".dll")
                        throw new FileLoadException("Invalid plugin file specified. Must be a DLL file");

                    String plugin_path = Path.Combine(Directories.Plugins, info.Name);

                    if (!Directory.Exists(plugin_path))
                        Directory.CreateDirectory(plugin_path);

                    info.CopyTo(plugin_path, true);
                    return LoadPlugin(info.Name);
                }
                catch (Exception ex) {
                    OnError("ImportPlugin", "ImportPlugin", ex);
                }
            }

            return false;
        }

        public void KillPlugin(string name) {
            Plugin plugin = null;

            lock (plugins) {
                string lowname = name.ToLower();
                int index = plugins.FindIndex(s => s.Name.ToLower() == lowname);

                if (index == -1) return;

                plugin = plugins[index];

                if (plugin.Enabled) {

                    plugin.Enabled = false;
                    RaisePropertyChanged(() => Count);

                    try {
                        plugin.Interface.OnPluginKilled();
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnPluginKilled", ex);
                    }
                }
            }
        }


        #region " Plugin Functions "

        public void OnError(String name, String method, Exception ex) {
            lock (plugins) {
                ErrorInfo error = new ErrorInfo(name, method, ex);

                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnError(error);
                    }
                    catch { /* ignore errors here.. stack overflow */ }
                }
            }
        }

        private void OnError(Plugin plugin, String method, Exception ex) {
            OnError(plugin.Name, method, ex);
        }


        public void OnCaptcha(IClient client, CaptchaEvent @event) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        plugin.Interface.OnCaptcha(client, @event);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnCaptcha", ex);
                    }
                }
            }
        }


        public void OnJoinRejected(IClient client, RejectReason reason) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnJoinRejected(client, reason);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnRejected", ex);
                    }
                }
            }
        }

        public void OnSendLogin(IClient client) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnSendLogin(client);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnSendLogin", ex);
                    }
                }
            }
        }

        public bool OnJoinCheck(IClient client) {
            bool ret = true;

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled && !plugin.Interface.OnJoinCheck(client))
                            ret = false;
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnJoinCheck", ex);
                    }
                }
            }

            return ret;
        }

        public void OnJoin(IClient client) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnJoin(client);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnJoin", ex);
                    }
                }
            }
        }

        public void OnPart(IClient client, Object state) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnPart(client, state);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnPart", ex);
                    }
                }
            }
        }


        public bool OnVroomJoinCheck(IClient client, UInt16 vroom) {
            bool ret = true;

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled && !plugin.Interface.OnVroomJoinCheck(client, vroom))
                            ret = false;
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnVroomJoinCheck", ex);
                    }
                }
            }

            return ret;
        }

        public void OnVroomJoin(IClient client) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnVroomJoin(client);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnVroomJoin", ex);
                    }
                }
            }
        }

        public void OnVroomPart(IClient client) {
            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnVroomPart(client);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnVroomPart", ex);
                    }
                }
            }
        }


        public bool OnRegister(IClient client, IPassword password) {
            bool ret = true;

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled && !plugin.Interface.OnRegister(client, password))
                            ret = false;
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnRegister", ex);
                    }
                }
            }

            return ret;
        }

        public bool OnFileReceived(IClient client, ISharedFile file) {
            bool ret = true;

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled && !plugin.Interface.OnFileReceived(client, file))
                            ret = false;
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnFileReceived", ex);
                    }
                }
            }

            return ret;
        }


        public bool OnBeforePacket(IClient client, IPacket packet) {
            bool ret = true;
            IPacket clone = packet.Clone();

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled && !plugin.Interface.OnBeforePacket(client, clone))
                            ret = false;

                        packet = clone;
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnBeforePacket", ex);
                    }
                }
            }

            return ret;
        }

        public void OnAfterPacket(IClient client, IPacket packet) {

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled)
                            plugin.Interface.OnAfterPacket(client, packet);
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnAfterPacket", ex);
                    }
                }
            }
        }

        public bool OnFlood(IClient client, IPacket packet) {
            bool ret = true;

            lock (plugins) {
                foreach (var plugin in plugins) {
                    try {
                        if (plugin.Enabled && !plugin.Interface.OnFlood(client, packet))
                            ret = false;
                    }
                    catch (Exception ex) {
                        OnError(plugin, "OnFlood", ex);
                    }
                }
            }

            return ret;
        }

        #endregion


        public IEnumerator<IPlugin> GetEnumerator() {
            return new PluginEnumerator(plugins);
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return (IEnumerator)GetEnumerator();
        }


        private void Wrapper_PropertyChanged(object sender, PropertyChangedEventArgs e) {
            var x = this.PropertyChanged;
            if (x != null) x(this, e);
        }

        private void Wrapper_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {

            var x = this.CollectionChanged;

            if (x != null) {
                
                switch (e.Action) {
                    case NotifyCollectionChangedAction.Add: {
                            Plugin[] newitems = new Plugin[e.NewItems.Count];

                            e.NewItems.CopyTo(newitems, 0);

                            x(this, new NotifyCollectionChangedEventArgs(
                                e.Action,
                                newitems.Select((s) => (IPlugin)s.Interface).ToList(),
                                e.NewStartingIndex));
                        }
                        break;
                    case NotifyCollectionChangedAction.Remove: {
                            Plugin[] olditems = new Plugin[e.OldItems.Count];

                            e.OldItems.CopyTo(olditems, 0);

                            x(this, new NotifyCollectionChangedEventArgs(
                                e.Action,
                                olditems.Select((s) => (IPlugin)s.Interface).ToList(),
                                e.OldStartingIndex));
                        }
                        break;
                    case NotifyCollectionChangedAction.Move: {
                            Plugin[] olditems = new Plugin[e.OldItems.Count];

                            e.OldItems.CopyTo(olditems, 0);

                            x(this, new NotifyCollectionChangedEventArgs(
                                e.Action,
                                olditems.Select((s) => (IPlugin)s.Interface).ToList(),
                                e.NewStartingIndex,
                                e.OldStartingIndex));
                        }
                        break;
                    case NotifyCollectionChangedAction.Replace: {
                            Plugin[] newitems = new Plugin[e.NewItems.Count];
                            Plugin[] olditems = new Plugin[e.OldItems.Count];

                            e.NewItems.CopyTo(newitems, 0);
                            e.OldItems.CopyTo(olditems, 0);

                            x(this, new NotifyCollectionChangedEventArgs(
                                e.Action,
                                newitems.Select((s) => (IPlugin)s.Interface).ToList(),
                                olditems.Select((s) => (IPlugin)s.Interface).ToList()));
                        }
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        x(this, new NotifyCollectionChangedEventArgs(e.Action));
                        break;
                }
            }
        }


        class PluginEnumerator : IEnumerator<IPlugin> 
        {
            int index = -1;
            ObservableCollection<Plugin> plugins;


            public IPlugin Current {
                get { return plugins[index].Interface; }
            }

            object IEnumerator.Current {
                get { return (object)Current; }
            }


            public PluginEnumerator(ObservableCollection<Plugin> plugins) {
                this.plugins = plugins;
            }


            public bool MoveNext() {

                if (++index >= plugins.Count)
                    return false;

                return true;
            }

            public void Reset() {
                index = -1;
            }

            public void Dispose() {
                plugins = null;
            }
        }


        #region " INotifyPropertyChanged "

        protected virtual void RaisePropertyChanged<T>(Expression<Func<T>> property) {
            var prop = ((MemberExpression)property.Body).Member as PropertyInfo;

            if (prop == null) return;
            OnPropertyChanged(prop.Name);
        }

        protected virtual void RaisePropertyChanged(string propName) {
            OnPropertyChanged(propName);
        }

        protected virtual void OnPropertyChanged(string propName) {
            var x = PropertyChanged;
            if (x != null) x(this, new PropertyChangedEventArgs(propName));
        }

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        public event NotifyCollectionChangedEventHandler CollectionChanged;
    }
}
